/*
 * User handling
 *
 * Copyright 2024 Odin Kroeger.
 *
 * This file is part of suCGI.
 *
 * suCGI is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * suCGI is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with suCGI. If not, see <https://www.gnu.org/licenses/>.
 */

#define _XOPEN_SOURCE 700

#if defined(__OPTIMIZE__) && !defined(_FORTIFY_SOURCE)
#define _FORTIFY_SOURCE 3
#endif

#include <sys/types.h>
#include <assert.h>
#include <errno.h>
#include <pwd.h>
#include <stddef.h>
/* cppcheck-suppress misra-c2012-21.6; needed for memcpy and snprintf */
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include "params.h"
#include "macros.h"
#include "types.h"
#include "user.h"


/*
 * Prototypes
 */

/*
 * Convert the given number to a string and write that string to the memory
 * area pointed to by "dst", which must be at least of the given size, then
 * move that pointer past the final byte written and deduct the number of
 * bytes written from the variable pointed to by "dstsz".
 *
 * Return values:
 *      OK       Success.
 *      ERR_LEN  Number is too large to fit.
 *      ERR_SYS  System error; errno should be set.
 */
static Error ll_cat(long long num, size_t *dstsz, char **dst);

/*
 * Copy the given string, which must be precisely of the given length,
 * to the memory pointed to by "dst", which must be at least of the given
 * size, then move that pointer past the final byte written and deduct
 * the number of bytes written from the variable pointed to by "dstsz".
 *
 * Return values:
 *      OK       Success.
 *      ERR_LEN  Number is too large to fit.
 */
static Error str_cat(size_t len, const char *src, size_t *dstsz, char **dst);

/*
 * Same as ll_cat, but for unsigned numbers.
 */
static Error ull_cat(unsigned long long num, size_t *dstsz, char **dst);


/*
 * Global functions
 */

Error
user_exp(const char *fmt, const struct passwd *const user,
         const size_t bufsize, char *const buf, size_t *len)
{
    assert(fmt != NULL);
    assert(strnlen(fmt, MAX_STR_LEN) < MAX_STR_LEN);
    assert(user != NULL);
    /* MAX_STR_LEN is <= PTRDIFF_MAX */
    assert(bufsize <= MAX_STR_LEN);
    assert(buf != NULL);

    const char *fmtp = fmt;     /* Current position in format string */
    char *bufp = buf;           /* Current position in buffer */
    size_t bufsp = bufsize;     /* Remaining space in buffer */
    size_t fidx;                /* Position of next "%" */
    Error retval;

    const size_t dirlen = strnlen(user->pw_dir, MAX_FNAME_LEN);
    if ((uintmax_t) dirlen >= (uintmax_t) MAX_FNAME_LEN) {
        return ERR_BAD;
    }

    const size_t namelen = strnlen(user->pw_name, MAX_LOGNAME_LEN);
    /* cppcheck-suppress misra-config; MAX_LOGNAME_LEN is not found */
    if ((uintmax_t) namelen >= (uintmax_t) MAX_LOGNAME_LEN) {
        return ERR_BAD;
    }

    (void) memset(buf, '\0', bufsize);

    do {
        fidx = strcspn(fmtp, "%");
        assert(fidx <= PTRDIFF_MAX);

        if (fidx >= bufsp) {
            return ERR_LEN;
        }

        /* RATS: ignore; checked for overflow above */
        (void) memcpy(bufp, fmtp, fidx);

        /* cppcheck-suppress [misra-c2012-10.3, misra-c2012-18.4]; safe */
        bufp += (ptrdiff_t) fidx;
        /* cppcheck-suppress [misra-c2012-10.3, misra-c2012-18.4]; safe */
        fmtp += (ptrdiff_t) fidx;
        bufsp -= fidx;

        if (*fmtp == '\0') {
            if (len != NULL) {
                /* cppcheck-suppress misra-c2012-18.4; safe */
                ptrdiff_t nbs = bufp - buf;
                assert(nbs >= 0);
                assert((uintmax_t) nbs < (uintmax_t) SIZE_MAX);
                assert(strnlen(buf, MAX_STR_LEN) == (size_t) nbs);

                *len = (size_t) nbs;
            }
            return OK;
        }

        switch (fmtp[1]) {
        case '%':
            retval = str_cat(1, "%", &bufsp, &bufp);
            if (retval != OK) {
                return retval;
            }
            break;
        case 'd':
            retval = str_cat(dirlen, user->pw_dir, &bufsp, &bufp);
            if (retval != OK) {
                return retval;
            }
            break;
        case 'g':
            /* cppcheck-suppress cert-INT31-c; not an integer conversion */
            if (ISSIGNED(gid_t)) {
                retval = ll_cat(user->pw_gid, &bufsp, &bufp);
            } else {
                retval = ull_cat(user->pw_gid, &bufsp, &bufp);
            }
            if (retval != OK) {
                return retval;
            }
            break;
        case 'n':
            retval = str_cat(namelen, user->pw_name, &bufsp, &bufp);
            if (retval != OK) {
                return retval;
            }
            break;
        case 'u':
            /* cppcheck-suppress misra-config; uid_t mistaken for variable */
            if (ISSIGNED(uid_t)) {
                retval = ll_cat(user->pw_uid, &bufsp, &bufp);
            } else {
                retval = ull_cat(user->pw_uid, &bufsp, &bufp);
            }
            if (retval != OK) {
                return retval;
            }
            break;
        default:
            return ERR_ARGS;
        }
 
        fmtp = &fmtp[2];
    } while (fidx < bufsize);

    return ERR_LEN;
}


/*
 * Module functions
 */

static Error
ll_cat(long long num, size_t *dstsz, char **dst)
{
    errno = 0;
    /* RATS: ignore; used safely. */
    int nbs = snprintf(NULL, 0, "%lld", num);
    if (nbs < 0) {
        return ERR_SYS;
    }

    /* MAX_STR_LEN is <= PTRDIFF_MAX and SIZE_MAX */
    assert((uintmax_t) nbs < MAX_STR_LEN);

    if ((size_t) nbs >= *dstsz) {
        return ERR_LEN;
    }

    errno = 0;
    /* RATS: ignore; checked for overflow above */
    nbs = snprintf(*dst, MAX_STR_LEN, "%lld", num);
    if (nbs < 0) {
        return ERR_SYS;
    }

    /* MAX_STR_LEN is <= PTRDIFF_MAX and SIZE_MAX */
    assert((uintmax_t) nbs < MAX_STR_LEN);

    /* cppcheck-suppress [misra-c2012-10.3, misra-c2012-18.4]; safe */
    *dst += (ptrdiff_t) nbs;
    *dstsz -= (size_t) nbs;

    return OK;
}

static Error
str_cat(const size_t len, const char *const src,
       size_t *const dstsz, char **const dst)
{
    /* MAX_STR_LEN is <= PTRDIFF_MAX */
    assert(len <= MAX_STR_LEN);

    if (len >= *dstsz) {
        return ERR_LEN;
    }

    /* RATS: ignore; checked for overflow above */
    (void) memcpy(*dst, src, len);

    /* cppcheck-suppress [misra-c2012-10.3, misra-c2012-18.4]; safe */
    *dst += (ptrdiff_t) len;
    *dstsz -= len;

    return OK;
}

static Error
ull_cat(unsigned long long num, size_t *dstsz, char **dst)
{
    /* RATS: ignore; used safely. */
    int nbs = snprintf(NULL, 0, "%llu", num);
    if (nbs < 0) {
        return ERR_SYS;
    }

    /* MAX_STR_LEN is <= PTRDIFF_MAX and SIZE_MAX */
    assert((uintmax_t) nbs < MAX_STR_LEN);

    if ((size_t) nbs >= *dstsz) {
        return ERR_LEN;
    }

    /* RATS: ignore; checked for overflow above */
    nbs = snprintf(*dst, MAX_STR_LEN, "%llu", num);
    if (nbs < 0) {
        return ERR_SYS;
    }

    /* MAX_STR_LEN is <= PTRDIFF_MAX and SIZE_MAX */
    assert((uintmax_t) nbs < MAX_STR_LEN);

    /* cppcheck-suppress [misra-c2012-10.3, misra-c2012-18.4]; safe */
    *dst += (ptrdiff_t) nbs;
    *dstsz -= (size_t) nbs;

    return OK;
}
